今天大概會聊到的範圍
- Snapshot system
 
上一篇有提到,State 改變時會觸發 recomposition。視這個行為是一種定義。但是為什麼?這怎麼發生的呢?
今天要介紹的是 Compose 中另一個與 State 息息相關的概念 - Snapshot
Snapshot 可以想像是當下所有 State 的存檔、快照。就像遊戲存檔或是電腦備份一樣,當下是什麼就是什麼,全部存起來。
提醒:Snapshot 通常不會用在 Compose 的開發中,主要是在用於 Compose 的內部運作。
讓我們看看 Snapshot 實際上怎麼使用的:
fun main() {
    // 1. 
    val data = mutableStateOf("") 
    
    // 2.
    data.value = "Foo"
    println("2: ${data.value}")
    
    // 3.
    val snap = Snapshot.takeSnapshot()
    
    // 4.
    data.value = "Bar"
    println("4: ${data.value}")
    
    // 5. 
    snap.enter {
        println("5: ${data.value}")
    }
    // 6.
    println("6: ${data.value}")
    
    
    // 7.
    snap.dispose()
}
Snapshot 和 State 的運作,是不一定要在 composable function 中執行的
Output:
2: Foo
4: Bar
5: Foo
6: Bar
mutableStateOf 建立 State。Snapshot.enter 可以 access 當時拍的 snapshot,在這個 scope 內,我們可以取得當時的 statedispose 釋放掉資源Snapshot 的概念就是這麼單純:保留某一個時刻的所有 State。
在 enter snapshot 後,如果我們還想對 State 進行編輯,將會得到錯誤。
    snap.enter {
        data.value = "Buzz"  // << --- Error
    }
在 snapshot 中,State 是唯讀無法編輯的。若希望編輯,我們會需要 MutableSnapshot
fun main() {
    val data = mutableStateOf("")
    
    data.value = "Foo"
    
    // 1.
    val mutableSnap = Snapshot.takeMutableSnapshot()
    
    // 2.
    mutableSnap.enter {
        println("2: ${data.value}")
        data.value = "Buzz"
        println("3: ${data.value}")
    }
    
    
    println("4: ${data.value}")
    
    // 5.
    mutableSnap.enter {
        println("5: ${data.value}")
    }
    
    // 6.
    mutableSnap.apply()
    println("6: ${data.value}")
    
    mutableSnap.dispose()
}
Output:
2: Foo
3: Buzz
4: Foo
5: Buzz
6: Buzz
takeMutableSnapshot 來建立 MutableSnapshot
MutableSnapshot 中,我們一樣可以拿到 takeSnapshot 時的資料 "Foo"MutableSnapshot 中,我們可以對 Sate 進行編輯MutableSnapshot.apply() 我們可以將 snapshot 的值實際賦予到 State 身上takeMutableSnapshot 和 apply 之間,有人直接對 State set value,apply 也不會發生作用fun takeMutableSnapshot(
    readObserver: ((Any) -> Unit)? = null,
    writeObserver: ((Any) -> Unit)? = null
): MutableSnapshot =
    (currentSnapshot() as? MutableSnapshot)?.takeNestedMutableSnapshot(
        readObserver,
        writeObserver
    ) ?: error("Cannot create a mutable snapshot of an read-only snapshot")
其實仔細看看 takeMutableSnapshot 的 signature,會發現其實他還有吃兩個參數:read/write 的 Observer。
實際上我們可以透過這兩個角色去監聽到資料被寫入、被讀取的時機,並且取得即將被異動的 State 與其資料。
fun main() {
    val data = mutableStateOf("")
    
    data.value = "Foo"
    
    val readObserver = { readState: Any -> if (readState == data) println("READ")}
    
    val writeObserver = { readState: Any -> if (readState == data) println("WRITE")}
    
    val mutableSnap = Snapshot.takeMutableSnapshot(readObserver, writeObserver)
    
    mutableSnap.enter {
        println("1: ${data.value}")
        data.value = "Buzz"
        println("2: ${data.value}")
    }
    
    println("3: ${data.value}")
    
    mutableSnap.apply()
    println("4: ${data.value}")
    
    mutableSnap.dispose()
}
READ
1: Foo
WRITE
READ
2: Buzz
3: Foo
4: Buzz
在
println前,我們需要去讀取data.value並將資料喂給printlnfunction ,因此每次在 snapshot 內的println之前都會觸發 READ
Composable 最後會被丟到 Recomposer 去執行,在 Recomposer 中我們可以發現這段 code
private inline fun <T> composing(
    composition: ControlledComposition,
    modifiedValues: IdentityArraySet<Any>?,
    block: () -> T
): T {
    val snapshot = Snapshot.takeMutableSnapshot(
        readObserverOf(composition), writeObserverOf(composition, modifiedValues)
    )
    try {
        return snapshot.enter(block)
    } finally {
        applyAndCheck(snapshot)
    }
}
private fun readObserverOf(composition: ControlledComposition): (Any) -> Unit {
    return { value -> composition.recordReadOf(value) }
}
// 
override fun recordReadOf(value: Any) {
    if (!areChildrenComposing) {
        composer.currentRecomposeScope?.let {    // <--- 
            it.used = true
            observations.add(value, it)
            
            ... 
        }
    }
}
Recomposer 在 composing 時,利用 snapshot 的 read/write observer 關注在 snapshot 中的 state 變化。並且在 read observer 時建立 recompose scope。
private fun writeObserverOf(
    composition: ControlledComposition,
    modifiedValues: IdentityArraySet<Any>?
): (Any) -> Unit {
    return { value ->
        composition.recordWriteOf(value)
        modifiedValues?.add(value)
    }
}
override fun recordWriteOf(value: Any) = synchronized(lock) {
    // ...
    derivedStates.forEachScopeOf(value) { // <--- 
        invalidateScopeOfLocked(it)
    }
}
在資料變動時,會 iterate 各個 scope 並且將他們 invalidate。
今天聊到的東西非常的底層,因為篇幅的關係,有些邏輯沒有說明到。非常推薦大家到下面 Reference 的地方閱讀本篇主要參考的文章。看了這些之後感覺對整個 compose 運作流程又再更近一步的認識了!
Reference: